﻿#region Copyright information
// 
// Copyright © 2012-2013 Yongkee Cho. All rights reserved.
// 
// This code is a part of the SubnetworkToolkit and governed under the terms of the
// GNU Lesser General  Public License (LGPL) version 2.1 which accompanies this distribution.
// For more information on the LGPL, please visit http://bol.codeplex.com/license.
// 
// - Filename: GreedySearch.cs
// - Author: Yongkee Cho
// - Email: yongkeecho@outlook.com
// - Date Created: 2013-01-30 2:55 PM
// - Last Modified: 2013-01-30 3:21 PM
// 
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using BOL.Algorithms;
using BOL.Linq.Descriptive;

namespace SubnetworkToolkit.GreedySearch
{
    public class GreedySearch : ISearch, IConvergeable, IVerbose
    {
        #region Private variable

        private readonly IDictionary<int, List<int>> _dictionary;
        private readonly IDictionary<int, double> _statistics;
        private readonly IList<int> _bypass; 

        #endregion

        #region Public properties

        public double Fraction { get; private set; }

        public int Degree { get; private set; }

        public double Tolerance { get; private set; }
        
        public bool IsVerbose { get; private set; }

        public int Depth { get; private set; }

        #endregion

        #region Constructor

        public GreedySearch(IDictionary<int, List<int>> dictionary, IDictionary<int, double> statistics, double tolerance, IList<int> bypass, int depth = 2, int degree = 3, double fraction = 1.0, bool isVerbose = true)
        {
            if (dictionary == null)
                throw new ArgumentNullException("dictionary");
            if (statistics == null)
                throw new ArgumentNullException("statistics");
            if (fraction < 0 || fraction > 1)
                throw new ArgumentOutOfRangeException("fraction");

            _dictionary = dictionary;
            _statistics = statistics;
            Degree = degree;
            Depth = depth;
            _bypass = bypass;
            Fraction = fraction;
            Tolerance = tolerance;
            IsVerbose = isVerbose;
        }

        #endregion

        #region Public methods

        public IEnumerable<Network> Search(int topN)
        {
            var i = 0;
            var result = new List<Network>();
            foreach (var gene in _dictionary.Keys)
                if (_dictionary[gene].Count() >= Degree)
                {
                    var subnet = new List<int> { gene };
                    var neighbors = new List<int>();
                    var depth = 0;
                    var score = Math.Abs(_statistics[gene]);
                    Search(gene, ref subnet, ref neighbors, ref depth, ref score);

                    var network = new Network(subnet.Select(x => new Gene(x, _statistics[x])));
                    if (!result.Contains(network))
                    {
                        if (IsVerbose)
                            Console.WriteLine("Network {0}: {1}", ++i, network);
                        result.Add(network);
                    }
                }

            var orderedResult = Fraction < 1.0 ? result.OrderByDescending(x => x.FractionMean(Fraction)) : result.OrderByDescending(x => x.Mean);
            return topN > 0 ? orderedResult.Take(topN) : orderedResult;
        }

        private void Search(int prev, ref List<int> subnet, ref List<int> neighbors, ref int depth, ref double score)
        {
            var newNeighbors = neighbors.Concat(_dictionary[prev]).Except(subnet).ToList();

            if (newNeighbors.Any())
            {
                int current;
                double currentScore;

                if (newNeighbors.Any(_bypass.Contains))
                {
                    current = newNeighbors.Intersect(_bypass).ArgMax(x => Math.Abs(_statistics[x]));
                    currentScore = score + 2 * score * Tolerance;
                }
                else
                {
                    current = newNeighbors.ArgMax(x => Math.Abs(_statistics[x]));
                    currentScore = score + Math.Abs(_statistics[current]);
                }

                subnet.Add(current);
                depth++;

                var diff = (currentScore - score) / score;

                if (diff > Tolerance || depth <= Depth)
                    Search(current, ref subnet, ref neighbors, ref depth, ref currentScore);
            }
        }

        #endregion
    }
}